﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Net.Nntp.Cache
{
    using System;
    using System.Collections.Generic;
    using System.Data.SqlServerCe;
    using System.Globalization;
    using System.IO;

    /// <summary>See <see cref="IGroup"/>.</summary>
    internal sealed class Group : IntIdObject<Group, IGroup>, IGroup
    {
        private const string GroupIdName = "@groupid";
        private const string IsOriginalBeingWatchedName = "@isOriginalBeingWatched";
        private const string HasOriginalBeenReadName = "@hasOriginalBeenRead";
        private const string IsFollowupBeingWatchedName = "@isFollowupBeingWatched";
        private const string HasFollowupBeenReadName = "@hasFollowupBeenRead";
        private const string QueryPrefix = "SELECT original.";

        private readonly Host host;
        private readonly string name;
        private string displayName;
        private int newInboundArticlesReceiveLimit;
        private int cachedInboundArticlesLimit;
        private long? selectedInboundArticleNumber;
        private InboundArticleFilter inboundArticleFilter;
        private readonly string dataDirectory;
        private string cachedOriginalsQueryBody;
        private string cachedFollowupsQueryBody;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IGroup.Name"/>.</summary>
        public string Name
        {
            get { return this.name; }
        }

        /// <summary>See <see cref="IGroup.DisplayName"/>.</summary>
        public string DisplayName
        {
            get
            {
                return this.displayName;
            }

            set
            {
                this.host.Database.GroupUpdater.UpdateDisplayName(this.Id, value);
                this.displayName = value;
            }
        }

        /// <summary>See <see cref="IGroup.NewInboundArticlesReceiveLimit"/>.</summary>
        public int NewInboundArticlesReceiveLimit
        {
            get
            {
                return this.newInboundArticlesReceiveLimit;
            }

            set
            {
                AssertValid(value, this.cachedInboundArticlesLimit);
                this.host.Database.GroupUpdater.UpdateNewInboundArticlesReceiveLimit(this.Id, value);
                this.newInboundArticlesReceiveLimit = value;
            }
        }

        /// <summary>See <see cref="IGroup.CachedInboundArticlesLimit"/>.</summary>
        public int CachedInboundArticlesLimit
        {
            get
            {
                return this.cachedInboundArticlesLimit;
            }

            set
            {
                AssertValid(this.newInboundArticlesReceiveLimit, value);
                this.host.Database.GroupUpdater.UpdateCachedInboundArticlesLimit(this.Id, value);
                this.cachedInboundArticlesLimit = value;
            }
        }

        /// <summary>See <see cref="IGroup.InboundArticleFilter"/>.</summary>
        public InboundArticleFilter InboundArticleFilter
        {
            get
            {
                return this.inboundArticleFilter;
            }

            set
            {
                this.host.Database.GroupUpdater.UpdateInboundArticleFilter(this.Id, value);
                this.inboundArticleFilter = value;
                FormatQueryBodies(
                    this.inboundArticleFilter, out this.cachedOriginalsQueryBody, out this.cachedFollowupsQueryBody);
            }
        }

        /// <summary>See <see cref="IGroup.CreateQueryCache"/>.</summary>
        public QueryCache CreateQueryCache()
        {
            return new QueryCache(this);
        }

        /// <summary>See <see cref="IGroup.GetOriginalInboundArticles(InboundArticleFilter)"/>.</summary>
        public IEnumerable<IInboundArticle> GetOriginalInboundArticles(InboundArticleFilter filter)
        {
            if (filter == InboundArticleFilter.NullFilter)
            {
                return InboundArticleSelector.GetArticles(this, null);
            }
            else
            {
                string originalsQueryBody;
                string followupsQueryBody;
                FormatQueryBodies(filter, out originalsQueryBody, out followupsQueryBody);
                return this.GetOriginalInboundArticlesCore(
                    this.CreateOriginalInboundArticlesCommand(filter, originalsQueryBody, followupsQueryBody), true);
            }
        }

        /// <summary>See <see cref="IGroup.GetThreadCount(InboundArticleFilter)"/>.</summary>
        public int GetThreadCount(InboundArticleFilter filter)
        {
            string originalsQueryBody;
            string followupsQueryBody;
            FormatQueryBodies(filter, out originalsQueryBody, out followupsQueryBody);
            return GetThreadCountCore(
                this.CreateThreadCountCommand(filter, originalsQueryBody, followupsQueryBody), true);
        }

        /// <summary>See <see cref="IGroup.MarkAllInboundArticlesAsRead"/>.</summary>
        public void MarkAllInboundArticlesAsRead()
        {
            const string CommandText =
                "UPDATE " + InboundArticleNames.Table + " SET " + InboundArticleNames.HasBeenReadColumn + " = 1 WHERE " +
                InboundArticleNames.GroupIdColumn + " = " + GroupIdName;

            using (var command = new SqlCeCommand(CommandText, this.Connection))
            {
                command.Parameters.Add(GroupIdName, this.Id);
                command.ExecuteNonQuery();
            }
        }

        /// <summary>See <see cref="IDeletablePersistentObject.Delete"/>.</summary>
        public void Delete()
        {
            this.host.Database.AssertSendReceiveNotInProgress();

            // This is necessary due to a cyclic relation between the Group and InboundArticle tables.
            this.DeleteAllInboundArticles();
            SqlCeHelper.Delete(this.Connection, GroupNames.Table, this.Id);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IGroup.Host"/>.</summary>
        IHost IGroup.Host
        {
            get { return this.Host; }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal Group(Host host, SqlCeDataReader reader) : base((int)reader[GroupNames.IdColumn])
        {
            this.host = host;

            this.name = (string)reader[GroupNames.NameColumn];
            this.displayName = (string)reader[GroupNames.DisplayNameColumn];
            this.newInboundArticlesReceiveLimit = (int)reader[GroupNames.NewInboundArticlesReceiveLimitColumn];
            this.cachedInboundArticlesLimit = (int)reader[GroupNames.CachedInboundArticlesLimitColumn];
            this.selectedInboundArticleNumber =
                reader.GetNullableColumn<long?>(GroupNames.SelectedInboundArticleNumberColumn);
            this.inboundArticleFilter = new InboundArticleFilter(
                reader.GetNullableColumn<bool?>(GroupNames.IsOriginalInboundArticleBeingWatchedFilterColumn),
                reader.GetNullableColumn<bool?>(GroupNames.HasOriginalInboundArticleBeenReadFilterColumn),
                reader.GetNullableColumn<bool?>(GroupNames.IsFollowupInboundArticleBeingWatchedFilterColumn),
                reader.GetNullableColumn<bool?>(GroupNames.HasFollowupInboundArticleBeenReadFilterColumn));

            this.dataDirectory = Path.Combine(
                Path.Combine(this.host.DataDirectory, "In"), this.Id.ToString(CultureInfo.InvariantCulture));
            FormatQueryBodies(
                this.inboundArticleFilter, out this.cachedOriginalsQueryBody, out this.cachedFollowupsQueryBody);
        }

        internal Host Host
        {
            get { return this.host; }
        }

        internal string DataDirectory
        {
            get { return this.dataDirectory; }
        }

        internal SqlCeConnection Connection
        {
            get { return this.host.Connection; }
        }

        internal long? SelectedInboundArticleNumber
        {
            get
            {
                return this.selectedInboundArticleNumber;
            }

            set
            {
                this.host.Database.GroupUpdater.UpdateSelectedInboundArticleNumber(this.Id, value);
                this.selectedInboundArticleNumber = value;
            }
        }

        internal void DeleteInboundArticleFile(long number)
        {
            File.Delete(InboundArticle.GetDataFilePath(this.dataDirectory, number));
        }

        internal void DeleteAllInboundArticles()
        {
            this.SelectedInboundArticleNumber = null;

            using (var command = CommandHelper.Create(
                this.Connection, InboundArticleNames.Table, DbRangeOptions.Prefix, new object[] { this.Id }, null))
            using (var resultSet = command.ExecuteResultSet(ResultSetOptions.Updatable | ResultSetOptions.Scrollable))
            using (var metaData = new MetaData(this.Host.Database))
            {
                int deletedCount = 0;

                while (resultSet.ReadLast())
                {
                    this.DeleteInboundArticleFile((long)resultSet[InboundArticleNames.NumberColumn]);
                    resultSet.Delete();
                    ++deletedCount;
                }

                metaData.InsertsAndDeletesSinceLastCompaction += deletedCount;
            }
        }

        internal IEnumerable<IInboundArticle> GetOriginalInboundArticlesCore(SqlCeCommand command, bool dispose)
        {
            try
            {
                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        yield return new InboundArticle(this, reader);
                    }
                }
            }
            finally
            {
                if (dispose)
                {
                    command.Dispose();
                }
            }
        }

        internal SqlCeCommand CreateOriginalInboundArticlesCommand(
            InboundArticleFilter filter, string originalsQueryBody, string followupsQueryBody)
        {
            var command = new SqlCeCommand(
                QueryPrefix + "*" + originalsQueryBody + "\r\nUNION\r\n" +
                QueryPrefix + "*" + followupsQueryBody + "\r\nORDER BY original.Number DESC",
                this.Connection);
            this.SetArticleCommandParameters(command, filter);
            return command;
        }

        internal SqlCeCommand CreateThreadCountCommand(
            InboundArticleFilter filter, string originalsQueryBody, string followupsQueryBody)
        {
            string commandText;

            if (filter == InboundArticleFilter.NullFilter)
            {
                commandText =
                    "SELECT COUNT(*) FROM " + InboundArticleNames.Table + " WHERE (" + InboundArticleNames.GroupIdColumn +
                    " = " + GroupIdName + ") AND " + InboundArticleNames.ReferencesNumberColumn + " IS NULL";
            }
            else
            {
                commandText =
                    "SELECT COUNT(Number) FROM\r\n" +
                    "(" + QueryPrefix + InboundArticleNames.NumberColumn + originalsQueryBody +
                    "\r\nUNION\r\n" +
                    QueryPrefix + InboundArticleNames.NumberColumn + followupsQueryBody + ") AS OriginalNumbers";
            }

            var command = new SqlCeCommand(commandText, this.Connection);
            this.SetArticleCommandParameters(command, filter);
            return command;
        }

        internal static void AssertValid(int newInboundArticlesReceiveLimit, int cachedInboundArticlesLimit)
        {
            if (newInboundArticlesReceiveLimit <= 0)
            {
                throw new ArgumentOutOfRangeException("newInboundArticlesReceiveLimit", "Positive number required.");
            }

            if (cachedInboundArticlesLimit < newInboundArticlesReceiveLimit)
            {
                throw new ArgumentOutOfRangeException(
                    "cachedInboundArticlesLimit", "Must be greater than or equal to newArticlesReceiveLimit.");
            }
        }

        internal static int GetThreadCountCore(SqlCeCommand command, bool dispose)
        {
            try
            {
                return (int)command.ExecuteScalar();
            }
            finally
            {
                if (dispose)
                {
                    command.Dispose();
                }
            }
        }

        internal static void FormatQueryBodies(
            InboundArticleFilter filter, out string originalsQueryBody, out string followupsQueryBody)
        {
            if (filter == InboundArticleFilter.NullFilter)
            {
                originalsQueryBody = null;
                followupsQueryBody = null;
            }
            else
            {
                originalsQueryBody = FormatQueryBody(
                    IsOriginalBeingWatchedName,
                    filter.IsOriginalBeingWatched,
                    HasOriginalBeenReadName,
                    filter.HasOriginalBeenRead,
                    false);
                followupsQueryBody = FormatQueryBody(
                    IsFollowupBeingWatchedName,
                    filter.IsFollowupBeingWatched,
                    HasFollowupBeenReadName,
                    filter.HasFollowupBeenRead,
                    true);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void SetArticleCommandParameters(SqlCeCommand articleCommand, InboundArticleFilter filter)
        {
            articleCommand.Parameters.Add(GroupIdName, this.Id);
            articleCommand.Parameters.Add(IsOriginalBeingWatchedName, filter.IsOriginalBeingWatched);
            articleCommand.Parameters.Add(HasOriginalBeenReadName, filter.HasOriginalBeenRead);
            articleCommand.Parameters.Add(IsFollowupBeingWatchedName, filter.IsFollowupBeingWatched);
            articleCommand.Parameters.Add(HasFollowupBeenReadName, filter.HasFollowupBeenRead);
        }

        private static string FormatQueryBody(
            string isBeingWatchedName,
            bool? isBeingWatched,
            string hasBeenReadName,
            bool? hasBeenRead,
            bool isFollowupPart)
        {
            var predicateTableName = isFollowupPart ? "followup" : "original";
            return
                " FROM " + InboundArticleNames.Table + " AS original " +
                (isFollowupPart ? "INNER JOIN " + InboundArticleNames.Table + " AS followup " : string.Empty) +
                "WITH(INDEX(" + (isBeingWatched == null ?
                    InboundArticleNames.GroupIdHasBeenReadIndex : InboundArticleNames.GroupIdIsBeingWatchedHasBeenReadIndex) + "))" +
                (isFollowupPart ?
                    " ON original." + InboundArticleNames.GroupIdColumn + " = followup." + InboundArticleNames.GroupIdColumn +
                    " AND original." + InboundArticleNames.NumberColumn + " = followup." + InboundArticleNames.OriginalNumberColumn :
                    string.Empty) +
                "\r\nWHERE (" + predicateTableName + "." + InboundArticleNames.GroupIdColumn + " = " + GroupIdName + ")" +
                (isBeingWatched != null ? " AND (" + predicateTableName + "." + InboundArticleNames.IsBeingWatchedColumn +
                    " = CONVERT(BIT, " + isBeingWatchedName + "))" : string.Empty) +
                (hasBeenRead != null ? " AND (" + predicateTableName + "." + InboundArticleNames.HasBeenReadColumn +
                    " = CONVERT(BIT, " + hasBeenReadName + "))" : string.Empty) +
                " AND (" + predicateTableName + "." + InboundArticleNames.ReferencesNumberColumn + " IS" +
                    (isFollowupPart ? " NOT " : " ") + "NULL)";
        }
    }
}
